// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#pragma once

#include <list>
#include <string>
#include <utility>
#include <vector>

#include "coin2/base/time.h"
#include "coin2/exchange/base/api_base/asio_http_context.h"
#include "coin2/exchange/base/api_base/base_auth.h"
#include "coin2/exchange/base/api_util/api_signature.h"
#include "coin2/exchange/base/api_util/auth_key.h"
#include "coin2/exchange/base/api_util/json_writer.h"
#include "coin2/exchange/base/api_util/url_params_builder.h"

class BitmexBaseAuth {
 public:
  virtual ~BitmexBaseAuth() = default;

  static std::string GetExpireSeconds() {
    std::string ts_str = std::to_string(GetCurrentTimestamp() / 1000000000 + 50);
    return ts_str;
  }

  static std::string GetSignature(
      const std::string& secret_key,
      const std::string& query_string,
      const std::string& post_body,
      const std::string& method,
      const std::string& time) {
    if (secret_key.empty()) {
      throw std::runtime_error("[Bitmex] API key and secret key are required");
    }
    std::string cre = method + query_string + time + post_body;
    return ApiSignatureUtil::GetSignatureByHexHmacSHA256(cre, secret_key);
  }
};

class BitmexWsAuth : public BitmexBaseAuth {
 public:
  explicit BitmexWsAuth(const std::string& key_filepath) : key_(AuthKey::FromFile(key_filepath)) {}

  const std::string CreateSignature() const {
    auto timestamp = GetExpireSeconds();
    std::string signa = GetSignature(
        key_.secret_key(),
        "/realtime",
        "",
        "GET",
        timestamp);
    return fmt::format(R"({{"op": "authKeyExpires", "args": ["{}", {}, "{}"]}})",
        key_.access_key(), timestamp, signa);
  }

 protected:
  AuthKey key_;
};

class BitmexRestAuth : public BitmexBaseAuth, public BaseRestAuth {
 public:
  explicit BitmexRestAuth(
      const std::string& key_filepath,
      const std::string& host,
      const std::string& port,
      const std::string& bind_ip)
      : key_(AuthKey::FromFile(key_filepath)), host_(host), port_(port), bind_ip_(bind_ip) {}
  explicit BitmexRestAuth(
      const std::string& key_filepath,
      const std::string& host,
      int32_t port,
      const std::string& bind_ip)
      : key_(AuthKey::FromFile(key_filepath)),
        host_(host),
        port_(std::to_string(port)),
        bind_ip_(bind_ip) {}

  AsioHttpContext* CreateRequestByGetWithSignature(
      const std::string& address,
      UrlParamsBuilder* builder) override {
    std::string query_string = builder->ComputeQueryString();
    std::string post_body = builder->ComputePostBody();
    CHECK(post_body.empty());
    std::string target;
    if (!query_string.empty()) {
      target = address + "?" + query_string;
    } else {
      target = address;
    }

    BuildHeader(
        key_.access_key(),
        key_.secret_key(),
        target,
        post_body,
        "GET",
        GetExpireSeconds());

    return AsioHttpContext::NewGet(host_, port_, bind_ip_, target, header_);
  }

  AsioHttpContext* CreateRequestByPostWithSignature(
      const std::string& address,
      UrlParamsBuilder* builder) override {
    std::string query_string = builder->ComputeQueryString();
    std::string post_body = builder->ComputePostBody();
    std::string target;
    if (!query_string.empty()) {
      target = address + "?" + query_string;
    } else {
      target = address;
    }

    BuildHeader(
        key_.access_key(),
        key_.secret_key(),
        target,
        post_body,
        "POST",
        GetExpireSeconds());

    return AsioHttpContext::NewPost(host_, port_, bind_ip_, target, header_, post_body);
  }

  AsioHttpContext* CreateRequestByDeleteWithSignature(
      const std::string& address,
      UrlParamsBuilder* builder) override {
    std::string query_string = builder->ComputeQueryString();
    std::string post_body = builder->ComputePostBody();
    std::string target;
    if (!query_string.empty()) {
      target = address + "?" + query_string;
    } else {
      target = address;
    }

    BuildHeader(
        key_.access_key(),
        key_.secret_key(),
        target,
        post_body,
        "DELETE",
        GetExpireSeconds());

    return AsioHttpContext::NewDelete(host_, port_, bind_ip_, target, header_, post_body);
  }

  AsioHttpContext* CreateRequestByPutWithSignature(
      const std::string& address,
      UrlParamsBuilder* builder) override {
    std::string query_string = builder->ComputeQueryString();
    std::string post_body = builder->ComputePostBody();
    std::string target;
    if (!query_string.empty()) {
      target = address + "?" + query_string;
    } else {
      target = address;
    }

    BuildHeader(
        key_.access_key(),
        key_.secret_key(),
        target,
        post_body,
        "PUT",
        GetExpireSeconds());

    return AsioHttpContext::NewPut(host_, port_, bind_ip_, target, header_, post_body);
  }

 private:
  void BuildHeader(
      const std::string& access_key,
      const std::string& secret_key,
      const std::string& query_path,
      const std::string& post_body,
      const std::string& method,
      const std::string& time) {
    header_.clear();
    std::string code = GetSignature(secret_key, query_path, post_body, method, time);
    header_.push_back(std::make_pair("api-expires", time));
    header_.push_back(std::make_pair("api-key", access_key));
    header_.push_back(std::make_pair("api-signature", code));
  }

 protected:
  AuthKey key_;
  std::string host_;
  std::string port_;
  std::string bind_ip_;
  std::vector<std::pair<std::string, std::string>> header_;
};
